package navigation;

import config.*;

/**
 * Base frame of a navigator
 *
 * @author hyurah
 */

public abstract class Navigator{

	protected Map map;
	protected int facingDirection;
	protected int xPosition;
	protected int yPosition;
	protected int xStart;
	protected int yStart;

	/**
	 * Constructs an Navigator with empty map data
	 */

	public Navigator() {
		map=new Map();
		facingDirection=Direction.SOUTH;
		xPosition=1;
		yPosition=0;
		xStart=-1;
		yStart=0;
		map.deadEndFound(xPosition, yPosition, facingDirection);
	}

	/**
	 * Constructs an Navigator  with data specified by the old navigator
	 *
	 * @param oldNavigator old navigator for data retrievel
	 */

	public Navigator(Navigator oldNavigator) {
		map=oldNavigator.map;
		facingDirection=oldNavigator.facingDirection;
		xPosition=oldNavigator.xPosition;
		yPosition=oldNavigator.yPosition;
		xStart=oldNavigator.xStart;
		yStart=oldNavigator.yStart;
	}

	/**
	 * Resets the map-position to the stating point
	 */

	public void resetToStartingPosition() {
		if(xStart<0) xPosition=1;
		else xPosition=xStart;
		yPosition=yStart;
	}

	/**
	 * Checks if the local field is known
	 *
	 * @return true if local field is known
	 */

	public boolean fieldIsKnown() {
		return map.fieldIsKnown(xPosition, yPosition);
	}

	/**
	 * Checks if the local field is an exit
	 *
	 * @return true if local field is an exit
	 */

	public boolean fieldIsExit() {
		return map.fieldIsExit(xPosition, yPosition);
	}

	/**
	 * Informs the navigator about a change of the facing-direction
	 *
	 * @param direction direction to turn to
	 */

	public void directionChanged(int direction) {
		facingDirection=(facingDirection+direction)%4;
	}

	/**
	 * Informs the navigator about a move forward
	 *
	 * @throws IllegalMoveException when the move leads out of the boundaries of the map
	 */

	public void moveOneStep() throws IllegalMoveException {
		switch(facingDirection) {
			case Direction.NORTH:
				if(yPosition-1<0) throw new IllegalMoveException(xPosition, yPosition-1);
				yPosition--;
				break;
			case Direction.EAST:
				if(xPosition+1>=map.getXSize()) throw new IllegalMoveException(xPosition+1, yPosition);
				xPosition++;
				break;
			case Direction.SOUTH:
				if(yPosition+1>=map.getYSize()) throw new IllegalMoveException(xPosition, yPosition+1);
				yPosition++;
				break;
			case Direction.WEST:
				if(xPosition-1<0) throw new IllegalMoveException(xPosition-1, yPosition);
				xPosition--;
				break;
		}
	}

	/**
	 * Adds the local field with the specified characteristics to the map
	 *
	 * @param forward field has a path forwards
	 * @param right field has a path to the right
	 * @param backward field has a path backwards
	 * @param left field has a path to the left
	 * @throws IllegalMapPositionException when current position is at the edge of the map
	 */

	public void addField(boolean forward, boolean right, boolean backward, boolean left) throws IllegalMapPositionException {
		if(xStart<0) {
			if(Config.FIRSTCURVEPOINTSTOMAZE) {
				if(left) {
					xStart=1;
					map.startKnown(Direction.WEST);
					map.setStart(xStart, yStart);
				} else if(right) {
					map.switchStartingLine();
					xStart=map.getXSize()-2;
					xPosition=map.getXSize()-2;
					map.startKnown(Direction.EAST);
					map.setStart(xStart, yStart);
				}
			} else {
				checkStartingLine();
			}
		}
		if(xPosition<1 || yPosition<1 || xPosition>map.getXSize()-2 || yPosition>map.getYSize()-2)
			throw new IllegalMapPositionException(xPosition, yPosition);
		switch(facingDirection) {
			case Direction.NORTH:
				map.addField(xPosition, yPosition, forward, right, backward, left);
				break;
			case Direction.EAST:
				map.addField(xPosition, yPosition, left, forward, right, backward);
				break;
			case Direction.SOUTH:
				map.addField(xPosition, yPosition, backward, left, forward, right);
				break;
			case Direction.WEST:
				map.addField(xPosition, yPosition, right, backward, left, forward);
				break;
		}
		if(!map.measuresKnown()) {
			checkMeasures(xPosition, yPosition);
		}
	}

	/**
	 * Sets field in front to contain an object
	 */

	public void objectFound() {
		if(map.fieldIsKnown(xPosition, yPosition))
			map.objectFound(xPosition, yPosition, facingDirection);
	}

	/**
	 * Sets local field to be a deadend
	 */

	public void deadEndFound() {
		map.deadEndFound(xPosition, yPosition, (facingDirection+2)%4);
	}

	/**
	 * Sets local field to be the red point
	 */

	public void redPointFound() {
		map.redPointFound(xPosition, yPosition);
	}

	/**
	 * Get string representation of the local surroundings
	 *
	 * @return string with facing direction position and paths
	 */

	@Override
	public String toString() {
		String direction = "";
		switch(facingDirection) {
			case Direction.NORTH: direction="North"; break;
			case Direction.EAST: direction="East"; break;
			case Direction.SOUTH: direction="South"; break;
			case Direction.WEST: direction="West"; break;
		}
		return "X:" + xPosition + " Y:" + yPosition + " Direction:" + direction + "\n"
				+ getLocalFieldDirectionState(0) + ", " + getLocalFieldDirectionState(1) + ", " + getLocalFieldDirectionState(2) + ", " + getLocalFieldDirectionState(3);
	}

	/**
	 * Get string representation of the map
	 *
	 * @return string with all fields of the map listed
	 */

	public String mapToString() {
		return map.toString();
	}

	/**
	 * Get the state of the specified direction from the local field
	 *
	 * @param direction direction of requested state
	 * @return state of specified field direction
	 */

	protected FieldDirectionState getLocalFieldDirectionState(int direction) {
		return map.getFieldDirectionState(xPosition, yPosition, direction);
	}

	/**
	 * Check if path to the specified direction exists
	 *
	 * @param direction direction of demanded path
	 * @return true if path to specified direction exists
	 */

	protected boolean isPassable(int direction) {
		return getLocalFieldDirectionState(direction)==FieldDirectionState.PATH;
	}

	/**
	 * Checks if the starting-position can be determined
	 */

	private void checkStartingLine() {
		if(xPosition<1) {
			map.switchStartingLine();
			xStart=map.getXSize()-2;
			xPosition=map.getXSize()-3;
			map.startKnown(Direction.EAST);
		} else if(xPosition>1) {
			xStart=1;
			map.startKnown(Direction.WEST);
		}
	}

	/**
	 * Check if map-measures can be determined
	 *
	 * @param x
	 * @param y
	 */

	private void checkMeasures(int x, int y) {
		int startX=0, endX=0, startY=0, endY=0;
		for(int i=0; i<Config.MAXFIELDS; i++) {
			if(map.fieldIsKnown(i, y)) startX=i;
			if(map.fieldIsKnown(x, i)) startY=i;
		}
		for(int i=Config.MAXFIELDS-1; i>=0; i--) {
			if(map.fieldIsKnown(i, y)) endX=i;
			if(map.fieldIsKnown(x, i)) endY=i;
		}
		if(endX-startX>Config.MINFIELDS-1) map.shrinkY();
		else if(endY-startY>Config.MINFIELDS-1)  {
			if(xStart!=0) {
				xStart-=Config.MAXFIELDS-Config.MINFIELDS;
				xPosition-=Config.MAXFIELDS-Config.MINFIELDS;
				map.shrinkX(false);
			} else map.shrinkX(true);
		}
	}

	/**
	 * Decide direction to drive to next
	 *
	 * @return direction to drive to or -1 if destination is reached
	 * @throws IllegalMapStateException when destination is not reachable in known map
	 */

	abstract public int decideDirection() throws IllegalMapStateException;

}
